/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package coalescent.br;

import static coalescent.br.BRWCompInfo.BRWState.*;

import java.util.*;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Walks up the recursion of the sample configuration backwards to its MRCA.
 *
 * @param <E>
 * @param <R>
 * @author Susanta Tewari
 * @version 2.0.0
 * @history Created on Created on 08/01/2011. Previous version was called ExactRecursion, created on
 * Jun 14, 2010, version 1.4.0.
 * @since 2.0.0
 */
public final class BRWalker<E extends Enum<E>, R extends AMCElement<E, R>> {


    // <editor-fold defaultstate="collapsed" desc="Support BRWComp">

    /**
     * event listeners observing the backward recursion transitionTypes
     */
    private List<BRWComp<E, R>> computations = new ArrayList<>(6);


    // <editor-fold defaultstate="collapsed" desc="Exe-Listeners">

    /** Field description */
    private final List<BRWalkerExeListener> exeListeners = new ArrayList<>(2);

    /**
     * to support computations based on the recursion run the last sample configuration
     */
    private R recursion;


    // <editor-fold defaultstate="collapsed" desc="Cache Management">

    /** Field description */
    private List<AMCElement>[] ancestralConfigCaches;

    /**
     * @param recursion the sample configuration for which the recursion is run
     */
    public BRWalker(R recursion) {

        this.recursion = recursion;

        initCache();
    }

    /**
     * all the listeners are freed after the recursion is complete and before this method returns
     */
    public void runRecursion() {


        // check if any listeners are attached, else stop
        if (computations.isEmpty()) {

            Logger.getLogger(getClass().getName()).log(Level.WARNING,
                             "No listeners attached to the exact recursion.");

            return;
        }

        ProgressReporter reporter = null;

        try {


            // schedule a progress reporter for long running recursions
            reporter = new ProgressReporter(10);


            // run the recursion
            recursion(recursion);
            fireComputation(new BRWCompInfo(this, FINISHED_RECURSION, recursion, null, null));
        } finally {


            // calcel the progress reporter
            if (reporter != null) {
                reporter.cancel();
            }


            // remove all the listeners
            computations.clear();
        }
    }

    /**
     * Implements the recursion
     *
     * @param recursion allele spectrum
     */
    private void recursion(R recursion) {

        if (recursion.isSink()) {

            addToCache(recursion);
            fireComputation(new BRWCompInfo<>(this, BOUNDARY_CONDN, recursion, null, null));

            return;
        }

        fireComputation(new BRWCompInfo<>(this, STARTING_ITERATING_EVENTS, recursion, null, null));
        fireExeEvent();

        for (E type : recursion.transitionTypes()) {

            Iterable<R> ancestralConfigs = recursion.transitions(type);

            if (!ancestralConfigs.iterator().hasNext()) {
                continue;
            }

            fireComputation(new BRWCompInfo<>(this, ITERATING_SINGLE_EVENT_TYPE, recursion, null,
                                              type));

            for (R ancestralConfig : ancestralConfigs) {

                if (!isInCache(ancestralConfig)) {
                    recursion(ancestralConfig);
                }

                fireComputation(new BRWCompInfo<>(this, ITERATING_ANCESTRAL_CONFIGS, recursion,
                                                  ancestralConfig, type));
            }

            fireComputation(new BRWCompInfo<>(this, FINISHED_ITERATING_SINGLE_EVENT_TYPE,
                                              recursion, null, type));
        }

        fireComputation(new BRWCompInfo<>(this, FINISHED_ITERATING_EVENTS, recursion, null, null));
        addToCache(recursion);
    }

    /**
     * Method description
     *
     */
    @SuppressWarnings("unchecked")
    private void initCache() {

        int cutSetCount = 0;

        cutSetCount = recursion.transitionIndex();


        // causes warning
        ancestralConfigCaches = new ArrayList[cutSetCount + 1];

        for (int i = 0; i < ancestralConfigCaches.length; i++) {

            ancestralConfigCaches[i] = new ArrayList<>();
        }
    }

    /**
     * Method description
     *
     *
     * @param sampleConfig
     */
    private void addToCache(AMCElement sampleConfig) {
        ancestralConfigCaches[sampleConfig.transitionIndex()].add(sampleConfig);
    }

    /**
     * Method description
     *
     *
     * @param ancestralConfig
     *
     * @return
     */
    private boolean isInCache(AMCElement ancestralConfig) {
        return ancestralConfigCaches[ancestralConfig.transitionIndex()].contains(ancestralConfig);
    }


    // </editor-fold>

    /**
     * Method description
     *
     *
     * @param compInfo
     */
    private void fireComputation(BRWCompInfo<E, R> compInfo) {

        for (BRWComp<E, R> listener : computations) {

            switch (compInfo.getRecursionEventType()) {

                case BOUNDARY_CONDN :
                    listener.onBoundaryCondn(compInfo);

                    break;

                case STARTING_ITERATING_EVENTS :
                    listener.onStartingIteratingEvents(compInfo);

                    break;

                case ITERATING_SINGLE_EVENT_TYPE :
                    listener.onIteratingSingleEventTypes(compInfo);

                    break;

                case ITERATING_ANCESTRAL_CONFIGS :
                    listener.onIteratingAncestralConfigs(compInfo);

                    break;

                case FINISHED_ITERATING_SINGLE_EVENT_TYPE :
                    listener.onFinishedIteratingSingleEventTypes(compInfo);

                    break;

                case FINISHED_ITERATING_EVENTS :
                    listener.onFinishedIteratingEvents(compInfo);

                    break;

                case FINISHED_RECURSION :
                    listener.onFinishedIteration(compInfo);

                    break;

                default :
                    Logger.getLogger(this.getClass().getName()).log(
                        Level.WARNING,
                        "" + "Missed enum type in switch statement, needs immediate inspection.");
            }
        }
    }

    /**
     * Method description
     *
     *
     * @param listener
     */
    public void addComputation(BRWComp listener) {

        if (computations.contains(listener)) {

            Logger.getLogger(getClass().getName()).log(Level.INFO,
                             "Computation " + listener
                             + " trying to register itself more than once: ");

        } else {
            computations.add(listener);
        }
    }

    /**
     * Method description
     *
     *
     * @param listener
     */
    public void removeComputation(BRWComp listener) {
        computations.remove(listener);
    }    // </editor-fold>

    /**
     * Method description
     *
     *
     * @param listener
     */
    public void addBRWalkerExeListener(BRWalkerExeListener listener) {

        if (exeListeners.contains(listener)) {

            Logger.getLogger(getClass().getName()).log(Level.INFO,
                             "Listener " + listener
                             + " trying to register itself more than once: ");

        } else {
            exeListeners.add(listener);
        }
    }

    /**
     * Method description
     *
     *
     * @param listener
     */
    public void removeBRWalkerExeListener(BRWalkerExeListener listener) {
        exeListeners.remove(listener);
    }

    /**
     * Method description
     *
     */
    private void fireExeEvent() {

        if (exeListeners.isEmpty()) {
            return;
        }


        // estimate cache size
        List<Integer> cacheSize = new ArrayList<>(ancestralConfigCaches.length);

        for (List<AMCElement> list : ancestralConfigCaches) {

            cacheSize.add(list.size());
        }


        // collect the computation chunks
        List<String> compChunks = new ArrayList<String>();

        for (BRWComp listener : computations) {

            if (listener.getUpdateChunk() != null) {
                compChunks.add(listener.getUpdateChunk());
            }
        }

        BRWalkerExeEvent event = new BRWalkerExeEvent(this, cacheSize, compChunks);

        for (BRWalkerExeListener listener : exeListeners) {

            listener.receivedExactRecursionExeEvent(event);
        }
    }


    // </editor-fold>

    /**
     * Method description
     *
     *
     * @return
     */
    public R getRecursion() {
        return recursion;
    }

    /**
     * Class description
     *
     *
     * @version        Enter version here..., 12/10/09
     * @author         Susanta Tewari
     */
    private class ProgressReporter {

        /** Field description */
        private Map<BRWComp, Timer> map = new HashMap<>();

        /**
         * @param seconds both the initial delay and period in seconds
         */
        public ProgressReporter(int seconds) {

            for (BRWComp listener : computations) {

                if (listener.createTimerTask() != null) {
                    map.put(listener, new Timer());
                }
            }


            // schedule the timers
            for (BRWComp listener : map.keySet()) {

                Timer timer = map.get(listener);

                timer.schedule(listener.createTimerTask(), seconds * 1000, seconds * 1000);
            }
        }

        void cancel() {

            for (BRWComp listener : map.keySet()) {

                Timer timer = map.get(listener);

                timer.cancel();
            }

            map.clear();
        }
    }
}
